199 lines · 8.3 KB
1 ---
2 import Repo from '../../../../layouts/Repo.astro';
3 import { apiGet, apiPost } from '../../../../lib/api';
4
5 const { owner, repo, number } = Astro.params;
6 const cookie = Astro.request.headers.get('cookie') || '';
7
8 let mr: any = null;
9 let error = '';
10
11 // Handle POST actions (comment or merge)
12 if (Astro.request.method === 'POST') {
13 try {
14 const formData = await Astro.request.formData();
15 const action = formData.get('action');
16
17 if (action === 'comment') {
18 const body = formData.get('body') as string;
19 await apiPost(
20 `/api/repos/${owner}/${repo}/merge-requests/${number}/comments`,
21 { body },
22 cookie
23 );
24 } else if (action === 'merge') {
25 await apiPost(
26 `/api/repos/${owner}/${repo}/merge-requests/${number}/merge`,
27 {},
28 cookie
29 );
30 }
31
32 return Astro.redirect(`/${owner}/${repo}/merge-requests/${number}`);
33 } catch (e: any) {
34 error = e.message;
35 }
36 }
37
38 try {
39 mr = await apiGet(`/api/repos/${owner}/${repo}/merge-requests/${number}`, cookie);
40 } catch (e: any) {
41 error = e.message;
42 }
43
44 function parseAuthor(author: string) {
45 const match = author?.match(/^(.+?) <(.+?)> (\d+)/);
46 if (!match) return { name: author || 'unknown', date: '' };
47 const date = new Date(parseInt(match[3]) * 1000);
48 return {
49 name: match[1],
50 date: date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }),
51 };
52 }
53 ---
54
55 <Repo owner={owner!} repo={repo!}>
56 {error && <div class="flash-error">{error}</div>}
57
58 {mr && (
59 <div>
60 {/* Header */}
61 <div style="margin-bottom: 20px;">
62 <h2 style="font-size: 1.5rem; margin-bottom: 4px;">
63 {mr.title}
64 <span style="color: var(--text-muted); font-weight: normal;"> #{mr.number}</span>
65 </h2>
66 <div style="display: flex; gap: 8px; align-items: center;">
67 <span style={`font-size: 0.75rem; padding: 2px 10px; border-radius: 12px; color: #fff; ${
68 mr.state === 'open' ? 'background: var(--state-open);'
69 : mr.state === 'merged' ? 'background: var(--state-merged);'
70 : 'background: var(--state-closed);'
71 }`}>
72 {mr.state}
73 </span>
74 <span style="font-size: 0.875rem; color: var(--text-muted);">
75 {mr.author_username} wants to merge
76 <code style="background: var(--bg-tertiary); padding: 1px 6px; border-radius: 4px;">{mr.source_branch}</code>
77 into
78 <code style="background: var(--bg-tertiary); padding: 1px 6px; border-radius: 4px;">{mr.target_branch}</code>
79 </span>
80 </div>
81 </div>
82
83 {/* Description */}
84 {mr.description && (
85 <div class="card" style="margin-bottom: 20px;">
86 <pre style="white-space: pre-wrap; font-family: var(--font-sans); font-size: 0.875rem;">{mr.description}</pre>
87 </div>
88 )}
89
90 {/* Diff */}
91 {mr.diff && mr.diff.length > 0 && (
92 <div style="margin-bottom: 20px;">
93 <h3 style="font-size: 1rem; margin-bottom: 12px;">
94 Files changed ({mr.diff.length})
95 </h3>
96 {mr.diff.map((file: any) => (
97 <div class="card" style="margin-bottom: 8px; padding: 0; overflow: hidden;">
98 <div style="padding: 8px 16px; background: var(--bg-tertiary); border-bottom: 1px solid var(--border); font-size: 0.8125rem; display: flex; gap: 8px; align-items: center;">
99 <span style={`padding: 0 6px; border-radius: 4px; font-size: 0.75rem; font-weight: 600; ${
100 file.status === 'added' ? 'background: var(--diff-add-badge-bg); color: var(--diff-add-text);'
101 : file.status === 'deleted' ? 'background: var(--diff-del-badge-bg); color: var(--diff-del-text);'
102 : 'background: var(--diff-mod-badge-bg); color: var(--diff-mod-text);'
103 }`}>
104 {file.status === 'added' ? 'A' : file.status === 'deleted' ? 'D' : 'M'}
105 </span>
106 <span style="font-family: var(--font-mono);">{file.path}</span>
107 </div>
108 {file.isBinary ? (
109 <div style="padding: 16px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">
110 Binary file
111 </div>
112 ) : (
113 <div style="overflow-x: auto;">
114 {file.hunks.map((hunk: any) => (
115 <table style="width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 0.8125rem;">
116 <tbody>
117 <tr>
118 <td colspan="3" style="padding: 4px 12px; background: var(--diff-hunk-bg); color: var(--diff-hunk-text); font-size: 0.75rem;">
119 @@ -{hunk.oldStart},{hunk.oldCount} +{hunk.newStart},{hunk.newCount} @@
120 </td>
121 </tr>
122 {hunk.lines.map((line: any) => (
123 <tr style={
124 line.type === 'add' ? 'background: var(--diff-add-bg);'
125 : line.type === 'delete' ? 'background: var(--diff-del-bg);'
126 : ''
127 }>
128 <td style="padding: 0 8px; text-align: right; color: var(--text-muted); user-select: none; width: 1%; white-space: nowrap; border-right: 1px solid var(--border);">
129 {line.oldLineNo ?? ''}
130 </td>
131 <td style="padding: 0 8px; text-align: right; color: var(--text-muted); user-select: none; width: 1%; white-space: nowrap; border-right: 1px solid var(--border);">
132 {line.newLineNo ?? ''}
133 </td>
134 <td style="padding: 0 12px; white-space: pre;">
135 <span style={
136 line.type === 'add' ? 'color: var(--diff-add-text);'
137 : line.type === 'delete' ? 'color: var(--diff-del-text);'
138 : ''
139 }>
140 {line.type === 'add' ? '+' : line.type === 'delete' ? '-' : ' '}{line.content}
141 </span>
142 </td>
143 </tr>
144 ))}
145 </tbody>
146 </table>
147 ))}
148 </div>
149 )}
150 </div>
151 ))}
152 </div>
153 )}
154
155 {/* Comments */}
156 <h3 style="font-size: 1rem; margin-bottom: 12px;">Discussion</h3>
157
158 {mr.comments?.map((comment: any) => (
159 <div class="card" style="margin-bottom: 8px;">
160 <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
161 <strong style="font-size: 0.875rem;">{comment.author_username}</strong>
162 <span style="font-size: 0.75rem; color: var(--text-muted);">{comment.created_at}</span>
163 </div>
164 {comment.file_path && (
165 <div style="font-size: 0.75rem; color: var(--text-muted); margin-bottom: 4px; font-family: var(--font-mono);">
166 {comment.file_path}:{comment.line_number}
167 </div>
168 )}
169 <div style="font-size: 0.875rem;">{comment.body}</div>
170 </div>
171 ))}
172
173 {/* Comment form */}
174 {mr.state === 'open' && (
175 <form method="POST" style="margin-top: 16px;">
176 <input type="hidden" name="action" value="comment" />
177 <div class="form-group">
178 <textarea name="body" rows="4" required placeholder="Leave a comment..."
179 style="font-family: var(--font-sans);"></textarea>
180 </div>
181 <div style="display: flex; gap: 8px; justify-content: flex-end;">
182 <button type="submit" class="btn btn-primary">Comment</button>
183 </div>
184 </form>
185 )}
186
187 {/* Merge button */}
188 {mr.state === 'open' && (
189 <form method="POST" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border);">
190 <input type="hidden" name="action" value="merge" />
191 <button type="submit" class="btn btn-primary" style="background: var(--state-merged); border-color: var(--state-merged);">
192 Merge merge request
193 </button>
194 </form>
195 )}
196 </div>
197 )}
198 </Repo>
199